PyQt里QWebEngineView内嵌网页与Python的数据交互

您所在的位置:网站首页 qt webview 交互 PyQt里QWebEngineView内嵌网页与Python的数据交互

PyQt里QWebEngineView内嵌网页与Python的数据交互

#PyQt里QWebEngineView内嵌网页与Python的数据交互| 来源: 网络整理| 查看: 265

数据交互需要load进一个网页,这里我选择load进一个本地html网页:JSTest.html。

同时,QWebEngineView与外面的交互还需要Qt官方提供的一个js文件:qwebchannel.js,这个文件可以在网上下载。

JSTest.html和qwebchannel.js两个文件放在同一个目录下,我这边都是放在Python工程目录下。

qwebchannel.js:

/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the examples of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of The Qt Company Ltd nor the names of its ** contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ "use strict"; var QWebChannelMessageTypes = { signal: 1, propertyUpdate: 2, init: 3, idle: 4, debug: 5, invokeMethod: 6, connectToSignal: 7, disconnectFromSignal: 8, setProperty: 9, response: 10, }; var QWebChannel = function(transport, initCallback) { if (typeof transport !== "object" || typeof transport.send !== "function") { console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); return; } var channel = this; this.transport = transport; this.send = function(data) { if (typeof(data) !== "string") { data = JSON.stringify(data); } channel.transport.send(data); } this.transport.onmessage = function(message) { var data = message.data; if (typeof data === "string") { data = JSON.parse(data); } switch (data.type) { case QWebChannelMessageTypes.signal: channel.handleSignal(data); break; case QWebChannelMessageTypes.response: channel.handleResponse(data); break; case QWebChannelMessageTypes.propertyUpdate: channel.handlePropertyUpdate(data); break; default: console.error("invalid message received:", message.data); break; } } this.execCallbacks = {}; this.execId = 0; this.exec = function(data, callback) { if (!callback) { // if no callback is given, send directly channel.send(data); return; } if (channel.execId === Number.MAX_VALUE) { // wrap channel.execId = Number.MIN_VALUE; } if (data.hasOwnProperty("id")) { console.error("Cannot exec message with property id: " + JSON.stringify(data)); return; } data.id = channel.execId++; channel.execCallbacks[data.id] = callback; channel.send(data); }; this.objects = {}; this.handleSignal = function(message) { var object = channel.objects[message.object]; if (object) { object.signalEmitted(message.signal, message.args); } else { console.warn("Unhandled signal: " + message.object + "::" + message.signal); } } this.handleResponse = function(message) { if (!message.hasOwnProperty("id")) { console.error("Invalid response message received: ", JSON.stringify(message)); return; } channel.execCallbacks[message.id](message.data); delete channel.execCallbacks[message.id]; } this.handlePropertyUpdate = function(message) { for (var i in message.data) { var data = message.data[i]; var object = channel.objects[data.object]; if (object) { object.propertyUpdate(data.signals, data.properties); } else { console.warn("Unhandled property update: " + data.object + "::" + data.signal); } } channel.exec({type: QWebChannelMessageTypes.idle}); } this.debug = function(message) { channel.send({type: QWebChannelMessageTypes.debug, data: message}); }; channel.exec({type: QWebChannelMessageTypes.init}, function(data) { for (var objectName in data) { var object = new QObject(objectName, data[objectName], channel); } // now unwrap properties, which might reference other registered objects for (var objectName in channel.objects) { channel.objects[objectName].unwrapProperties(); } if (initCallback) { initCallback(channel); } channel.exec({type: QWebChannelMessageTypes.idle}); }); }; function QObject(name, data, webChannel) { this.__id__ = name; webChannel.objects[name] = this; // List of callbacks that get invoked upon signal emission this.__objectSignals__ = {}; // Cache of all properties, updated when a notify signal is emitted this.__propertyCache__ = {}; var object = this; // ---------------------------------------------------------------------- this.unwrapQObject = function(response) { if (response instanceof Array) { // support list of objects var ret = new Array(response.length); for (var i = 0; i < response.length; ++i) { ret[i] = object.unwrapQObject(response[i]); } return ret; } if (!response || !response["__QObject*__"] || response.id === undefined) { return response; } var objectId = response.id; if (webChannel.objects[objectId]) return webChannel.objects[objectId]; if (!response.data) { console.error("Cannot unwrap unknown QObject " + objectId + " without data."); return; } var qObject = new QObject( objectId, response.data, webChannel ); qObject.destroyed.connect(function() { if (webChannel.objects[objectId] === qObject) { delete webChannel.objects[objectId]; // reset the now deleted QObject to an empty {} object // just assigning {} though would not have the desired effect, but the // below also ensures all external references will see the empty map // NOTE: this detour is necessary to workaround QTBUG-40021 var propertyNames = []; for (var propertyName in qObject) { propertyNames.push(propertyName); } for (var idx in propertyNames) { delete qObject[propertyNames[idx]]; } } }); // here we are already initialized, and thus must directly unwrap the properties qObject.unwrapProperties(); return qObject; } this.unwrapProperties = function() { for (var propertyIdx in object.__propertyCache__) { object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); } } function addSignal(signalData, isPropertyNotifySignal) { var signalName = signalData[0]; var signalIndex = signalData[1]; object[signalName] = { connect: function(callback) { if (typeof(callback) !== "function") { console.error("Bad callback given to connect to signal " + signalName); return; } object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; object.__objectSignals__[signalIndex].push(callback); if (!isPropertyNotifySignal && signalName !== "destroyed") { // only required for "pure" signals, handled separately for properties in propertyUpdate // also note that we always get notified about the destroyed signal webChannel.exec({ type: QWebChannelMessageTypes.connectToSignal, object: object.__id__, signal: signalIndex }); } }, disconnect: function(callback) { if (typeof(callback) !== "function") { console.error("Bad callback given to disconnect from signal " + signalName); return; } object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; var idx = object.__objectSignals__[signalIndex].indexOf(callback); if (idx === -1) { console.error("Cannot find connection of signal " + signalName + " to " + callback.name); return; } object.__objectSignals__[signalIndex].splice(idx, 1); if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { // only required for "pure" signals, handled separately for properties in propertyUpdate webChannel.exec({ type: QWebChannelMessageTypes.disconnectFromSignal, object: object.__id__, signal: signalIndex }); } } }; } /** * Invokes all callbacks for the given signalname. Also works for property notify callbacks. */ function invokeSignalCallbacks(signalName, signalArgs) { var connections = object.__objectSignals__[signalName]; if (connections) { connections.forEach(function(callback) { callback.apply(callback, signalArgs); }); } } this.propertyUpdate = function(signals, propertyMap) { // update property cache for (var propertyIndex in propertyMap) { var propertyValue = propertyMap[propertyIndex]; object.__propertyCache__[propertyIndex] = propertyValue; } for (var signalName in signals) { // Invoke all callbacks, as signalEmitted() does not. This ensures the // property cache is updated before the callbacks are invoked. invokeSignalCallbacks(signalName, signals[signalName]); } } this.signalEmitted = function(signalName, signalArgs) { invokeSignalCallbacks(signalName, signalArgs); } function addMethod(methodData) { var methodName = methodData[0]; var methodIdx = methodData[1]; object[methodName] = function() { var args = []; var callback; for (var i = 0; i < arguments.length; ++i) { if (typeof arguments[i] === "function") callback = arguments[i]; else args.push(arguments[i]); } webChannel.exec({ "type": QWebChannelMessageTypes.invokeMethod, "object": object.__id__, "method": methodIdx, "args": args }, function(response) { if (response !== undefined) { var result = object.unwrapQObject(response); if (callback) { (callback)(result); } } }); }; } function bindGetterSetter(propertyInfo) { var propertyIndex = propertyInfo[0]; var propertyName = propertyInfo[1]; var notifySignalData = propertyInfo[2]; // initialize property cache with current value // NOTE: if this is an object, it is not directly unwrapped as it might // reference other QObject that we do not know yet object.__propertyCache__[propertyIndex] = propertyInfo[3]; if (notifySignalData) { if (notifySignalData[0] === 1) { // signal name is optimized away, reconstruct the actual name notifySignalData[0] = propertyName + "Changed"; } addSignal(notifySignalData, true); } Object.defineProperty(object, propertyName, { configurable: true, get: function () { var propertyValue = object.__propertyCache__[propertyIndex]; if (propertyValue === undefined) { // This shouldn't happen console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); } return propertyValue; }, set: function(value) { if (value === undefined) { console.warn("Property setter for " + propertyName + " called with undefined value!"); return; } object.__propertyCache__[propertyIndex] = value; webChannel.exec({ "type": QWebChannelMessageTypes.setProperty, "object": object.__id__, "property": propertyIndex, "value": value }); } }); } // ---------------------------------------------------------------------- data.methods.forEach(addMethod); data.properties.forEach(bindGetterSetter); data.signals.forEach(function(signal) { addSignal(signal, false); }); for (var name in data.enums) { object[name] = data.enums[name]; } } //required for use with nodejs if (typeof module === 'object') { module.exports = { QWebChannel: QWebChannel }; }

JSTest.html:

//---Web show receive message--- function output(message) { var output = document.getElementById("output"); output.innerHTML = output.innerHTML + message + "\n"; } function showAlert() { alert('this is web alert'); } //Web initial loading window.onload = function() { new QWebChannel(qt.webChannelTransport, function(channel) { //Get Qt interact object var interactObj = channel.objects.interactObj; //Web send message to Qt document.getElementById("send").onclick = function() { var input = document.getElementById("input"); if (!input.value) { return; } output("Send string to Qt: " + input.value); //Web use the interface of Qt interactObj.fun(alert); interactObj.JSSendMessage(input.value); input.value = ""; } //Web connect the Qt signal, then Qt can call "output" function interactObj.SigSendMessageToJS.connect(function(str) { output("Received string from Qt: " + str); }); }); } html { height: 100%; width: 100%; } #input { width: 650px; margin: 0 10px 0 0; } #send { width: 90px; margin: 0; } #output { width: 770px; height: 550px; }

接下来是Python工程里的代码。

main.py:

from PyQt5.QtWidgets import QApplication import sys from TMainWindow import TMainWindow if __name__ == '__main__': app = QApplication(sys.argv) dlg = TMainWindow() dlg.show() app.exec_()

TMainWindow.py:

from PyQt5.QtWidgets import QDialog, QPlainTextEdit, QLineEdit, QPushButton, QHBoxLayout, QVBoxLayout from PyQt5.QtWidgets import QGroupBox from PyQt5.QtWebEngineWidgets import * from PyQt5.QtWebChannel import * from PyQt5.QtCore import QUrl, pyqtSignal from TInteractObject import TInteractObj class TMainWindow(QDialog): SigSendMessageToJS = pyqtSignal(str) def __init__(self, parent = None): super().__init__(parent) #---Qt widget and layout--- self.mpQtContentTextEdit = QPlainTextEdit(self) self.mpQtContentTextEdit.setMidLineWidth(400) self.mpQtContentTextEdit.setReadOnly(True) self.mpQtSendLineEdit = QLineEdit(self) self.mpQtSendBtnByInteractObj = QPushButton('Send', self) self.mpQtSendBtnByInteractObj.setToolTip('Send message by Interact object style') self.mpQtSendBtnByJavaScript = QPushButton('Send2', self) self.mpQtSendBtnByJavaScript.setToolTip('Send message by runJavaScript style') self.pQtSendHLayout = QHBoxLayout() self.pQtSendHLayout.setSpacing(0) self.pQtSendHLayout.addWidget(self.mpQtSendLineEdit) self.pQtSendHLayout.addSpacing(5) self.pQtSendHLayout.addWidget(self.mpQtSendBtnByInteractObj) self.pQtSendHLayout.addSpacing(5) self.pQtSendHLayout.addWidget(self.mpQtSendBtnByJavaScript) self.pQtTotalVLayout = QVBoxLayout() self.pQtTotalVLayout.setSpacing(0) self.pQtTotalVLayout.addWidget(self.mpQtContentTextEdit) self.pQtTotalVLayout.setSpacing(5) self.pQtTotalVLayout.addLayout(self.pQtSendHLayout) self.pQtGroup = QGroupBox('Qt View', self) self.pQtGroup.setLayout(self.pQtTotalVLayout) #---Web widget and layout--- self.mpJSWebView = QWebEngineView(self) self.pWebChannel = QWebChannel(self.mpJSWebView.page()) self.pInteractObj = TInteractObj(self) self.pWebChannel.registerObject("interactObj", self.pInteractObj) self.mpJSWebView.page().setWebChannel(self.pWebChannel) self.url = 'file:///D:/PyPro/PyQtJSInteract/JSTest.html' self.mpJSWebView.page().load(QUrl(self.url)) self.mpJSWebView.show() self.pJSTotalVLayout = QVBoxLayout() self.pJSTotalVLayout.setSpacing(0) self.pJSTotalVLayout.addWidget(self.mpJSWebView) self.pWebGroup = QGroupBox('Web View', self) self.pWebGroup.setLayout(self.pJSTotalVLayout) #---TMainWindow total layout--- self.mainLayout = QHBoxLayout() self.mainLayout.setSpacing(0) self.mainLayout.addWidget(self.pQtGroup) self.mainLayout.setSpacing(5) self.mainLayout.addWidget(self.pWebGroup) self.setLayout(self.mainLayout) self.setMinimumSize(1130, 680) self.mpQtSendBtnByInteractObj.clicked.connect(self.OnSendMessageByInteractObj) self.mpQtSendBtnByJavaScript.clicked.connect(self.OnSendMessageByJavaScript) self.pInteractObj.SigReceivedMessFromJS.connect(self.OnReceiveMessageFromJS) self.SigSendMessageToJS.connect(self.pInteractObj.SigSendMessageToJS) def OnReceiveMessageFromJS(self, strParameter): print('OnReceiveMessageFromJS()') if not strParameter: return self.mpQtContentTextEdit.appendPlainText(strParameter) def OnSendMessageByInteractObj(self): strMessage = self.mpQtSendLineEdit.text() if not strMessage: return self.SigSendMessageToJS.emit(strMessage) def OnSendMessageByJavaScript(self): strMessage = self.mpQtSendLineEdit.text() if not strMessage: return strMessage = 'Received string from Qt:' + strMessage self.mpJSWebView.page().runJavaScript("output(%s)" %strMessage) self.mpJSWebView.page().runJavaScript("showAlert()")

构造函数前半部分为控件及布局。

倒数第二行的runJavaScript函数没执行成功,没能成功传参数给JS那边,这个后续再跟进。不过在OnSendMessageByInteractObj(self)函数里能成功传参数给JS那边,这里用的是QWebChannel。而JS传参数给Python则用的是OnReceiveMessageFromJS(self, strParameter)函数。

TInteractObject.py:

from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot class TInteractObj(QObject): SigReceivedMessFromJS = pyqtSignal(str) SigSendMessageToJS = pyqtSignal(str) def __init__(self, parent = None): super().__init__(parent) @pyqtSlot(str) def JSSendMessage(self, strParameter): print('JSSendMessage(%s) from Html' %strParameter) self.SigReceivedMessFromJS.emit(strParameter) @pyqtSlot(result=str) def fun(self): print('TInteractObj.fun()') return 'hello'JS调用Python的函数必须是槽函数,否则Python这边没有响应,会给人一种没有收到的错觉。在C++里槽函数是public slots,而Python里则用@pyqtSlot来定义,后面括号内为参数类型。如果该槽函数有返回值,则在括号内加上“result=参数类型”。

运行后效果如下图:

之前有关PyQt QWebEngineView的资料比较少,搞了好久,现在终于搞好了。整理了后写了出来,希望对有需要的人有所帮助。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3